Перейти к основному содержимому

4.02. Теория кодирования

Разработчику Аналитику Тестировщику
Архитектору Инженеру

Теория кодирования

Когда мы говорим о кодировании в повседневной речи, чаще всего имеем в виду написание программ — преобразование человеческих намерений в последовательности инструкций, понятных машине. Однако это — лишь один, хотя и самый заметный, фрагмент гораздо более обширного феномена. Изначально теория кодирования родилась из вопросов вроде "Как передать мысль точно?", "Как хранить её надёжно?" и "Как гарантировать, что получатель воспримет то же самое, что и отправитель?".

Фактически, кодирование просто "перепаковывает информацию" с одного формата в другой.

Всякая передача или хранение информации есть её перемещение во времени и/или пространстве через канал, обладающий конечными и несовершенными свойствами. Канал может быть проводом, оптическим волокном, радиоэфиром, поверхностью магнитного диска, памятью на кристалле микросхемы — или даже человеческой речью в шумном помещении. Во всех этих случаях присутствуют три объективных фактора:

  1. Ограниченная пропускная способность — канал не может передавать бесконечно много данных в единицу времени (или хранить бесконечный объём в единице объёма).
  2. Шум и искажения — при прохождении через канал информация подвергается воздействию помех, в результате чего отдельные элементы могут быть искажены, потеряны или вставлены ложно.
  3. Несовместимость форматов — источник и получатель оперируют, как правило, разными внутренними представлениями; требуется согласование.

Теория кодирования предлагает системный подход к преодолению этих барьеров. Она показывает, что проблему можно разделить на три логические подзадачи, каждая из которых решается собственным классом кодов:

  • Представительское (структурное) кодирование обеспечивает однозначное и удобное отображение информации в форму, пригодную для обработки в конкретной технической среде — например, перевод текста в последовательность байтов (UTF-8), числа — в двоичную форму с плавающей точкой (IEEE 754), инструкции — в машинный код (x86, ARM). Здесь главный критерий — однозначность интерпретации.

  • Источниковое (сжимающее) кодирование устраняет избыточность, присущую данным на уровне их генерации — лингвистическую, статистическую, структурную. Его цель — уменьшить объём без потери содержания (как в ZIP) или с контролируемым искажением (как в JPEG). Здесь доминирует критерий эффективности: сколько информации приходится на единицу ресурса.

  • Канальное (помехоустойчивое) кодирование вводит целенаправленную избыточность для борьбы с ошибками, возникающими при передаче или хранении. Вместо того чтобы надеяться на идеальную среду, такой код проектируется так, чтобы даже при наличии искажений можно было восстановить оригинал — либо обнаружить ошибку и запросить повтор (как в TCP), либо исправить её на месте (как в ECC-памяти). Здесь решающим становится критерий надёжности.

На практике эти уровни редко изолированы: современные системы строятся как многослойные стеки кодирования. Например, видеопоток в стриминговом сервисе может пройти следующие этапы:

  1. Представление кадров в цветовом пространстве YCbCr →
  2. Сжатие без потерь на уровне макроблоков (H.264 intra-prediction) и с потерями через квантование ДКП →
  3. Энтропийное кодирование остаточных коэффициентов (CABAC — контекстно-адаптивное арифметическое кодирование) →
  4. Формирование транспортных пакетов с CRC-контрольными суммами →
  5. Помехоустойчивое кодирование (LDPC) →
  6. Модуляция в QAM-сигнал для передачи по Wi-Fi.

Каждый слой решает свою задачу, но все они вместе образуют единую стратегию управления информацией — от момента её возникновения до момента потребления.

Теория кодирования

Кодирование — одна из центральных концепций в информатике, связи и вычислительной технике. Оно охватывает программирование и шифрование, а также фундаментальные процессы представления, передачи и хранения информации в условиях ограниченных ресурсов, несовершенных каналов и неизбежного присутствия ошибок. Чтобы понять масштаб этого явления, необходимо начать с самого широкого, почти философского толкования понятия «код» — а затем последовательно сузить фокус до конкретных классов кодов, их целей и механизмов реализации.

Что такое «код» — от абстракции к практике

Слово «код» в обиходной речи используется довольно широко и, порой, нестрого: им могут называть программу, криптограмму, штриховой маркер на товаре или даже логин в мессенджере. Однако в теоретическом контексте код — это всегда система правил, сопоставляющих элементам одной совокупности (источника информации) элементы другой совокупности (представления информации). Иными словами, код — это отображение.

Это отображение может быть тривиальным (например, каждому символу латинского алфавита сопоставляется его порядковый номер от 1 до 26), а может быть крайне сложным (например, при сжатии видеопотока в формате H.265 или при кодировании данных на поверхности жёсткого диска). Важно понимать: кодирование не создаёт новой информации — оно лишь преобразует уже существующую, стремясь удовлетворить определённым внешним требованиям.

В зависимости от цели такого преобразования выделяют три основные категории кодирования:

  1. Структурирующее, или представительское кодирование — направлено на удобное и однозначное отображение информации в среду, где она будет храниться, обрабатываться или передаваться. Примеры: двоичное представление чисел в памяти компьютера, кодировки символов (ASCII, UTF-8), машинный язык процессора.

  2. Сжимающее, или источниковое кодирование — направлено на уменьшение объёма данных без потери содержания (в случае сжатия без потерь) либо с контролируемым искажением (в случае сжатия с потерями). Здесь код служит инструментом экономии ресурсов: времени передачи, ёмкости памяти, пропускной способности канала.

  3. Защитное, или помехоустойчивое кодирование — направлено на обеспечение целостности данных при их передаче или хранении в условиях возможного искажения. Такой код вводит избыточность, позволяющую обнаружить и исправить ошибки, возникшие из-за шумов, помех, сбоев или физического повреждения носителя.

На практике реальные системы обычно комбинируют все три подхода. Например, в цифровом телевидщении видеопоток сначала сжимается (MPEG), затем кодируется для защиты от ошибок (Reed–Solomon + LDPC), а после этого преобразуется в модулированный сигнал (QAM), адаптированный к физическим свойствам передающей среды (кабель, эфир, спутник). Каждый этап — это отдельное кодирование, служащее своей задаче.

Откуда вообще берётся необходимость кодировать?

Ответ лежит в природе самих информационных систем. Любая передача или обработка информации происходит через канал связи — физическую или логическую среду, соединяющую источник и получателя.

В идеализированной модели канал может быть безошибочным, бесконечно ёмким и мгновенным. Реальные каналы таковыми не являются. Они обладают конечной пропускной способностью, подвержены помехам (электромагнитным, термическим, механическим — в зависимости от носителя) и несут в себе задержки. Даже память компьютера — это канал: запись и чтение — это отправка и приём данных во времени.

Кроме того, сама информация, как правило, представлена в форме, не оптимальной для конкретного канала. Человек мыслит категориями слов и образов; компьютер же оперирует последовательностями битов. Устройства ввода и вывода (клавиатуры, дисплеи, принтеры) используют собственные алфавиты и протоколы. Чтобы «согласовать» все эти разнородные уровни, требуется трансляция — а значит, код.

Ключевое понятие, лежащее в основе всей теории кодирования, — это бит. Бит (binary digit) — наименьшая единица информации, способная принимать одно из двух значений: условно «0» или «1». Именно эта двоичность делает возможным построение устойчивых цифровых систем: два дискретных состояния легко различимы даже при наличии шумов (например, низкий и высокий уровень напряжения в логической схеме).

На практике единичный бит редко используется в «чистом» виде. Гораздо чаще информация группируется в байты — блоки по восемь бит. Байт стал де-факто стандартной ячейкой адресуемой памяти в подавляющем большинстве архитектур. Байт позволяет закодировать 256 различных значений, что достаточно для представления расширенных алфавитов, управляющих символов и базовых числовых типов.

Байты объединяются в более крупные структуры: машинные слова, кадры, пакеты, страницы. Но базовым «строительным элементом» остаётся бит — и именно с ним работает теория кодирования.

Кодовое слово, кодовая книга и характеристики кода

Чтобы сделать кодирование управляемым и однозначным, необходимо ввести формализм. Основной объект — кодовое слово: это конечная последовательность символов, принадлежащих некоторому алфавиту кода. В подавляющем большинстве современных систем этот алфавит двоичный — то есть состоит из двух символов: 0 и 1. Но теоретически возможны и троичные, четверичные и иные алфавиты (например, в ДНК-вычислениях используется четверичный алфавит A, C, G, T).

Совокупность всех допустимых кодовых слов образует код. Если каждому символу или блоку исходных данных сопоставляется ровно одно кодовое слово, то такое соответствие называют кодовой книгой. Кодовая книга — это, по сути, справочник (явный или неявный), по которому происходит преобразование.

Важнейшие количественные характеристики кода:

  • Длина кодового слова — количество символов (обычно битов) в одном кодовом слове. Может быть фиксированной (например, в кодировке ASCII каждый символ — ровно 8 бит) или переменной (например, в коде Хаффмана часто встречающиеся символы кодируются короткими словами, редкие — длинными).

  • Мощность кода — число различных кодовых слов, содержащихся в коде. Для двоичного кода длины n максимальная мощность равна 2n, но реальные коды, особенно помехоустойчивые, используют лишь часть этого пространства — именно за счёт такой «недогрузки» и достигается избыточность, необходимая для обнаружения и исправления ошибок.

  • Скорость кода — это мера эффективности кода, определяемая как отношение количества информационных бит к общему числу переданных бит в кодовом слове. Скорость всегда лежит в интервале от 0 до 1. Чем ближе скорость к единице, тем меньше избыточность и тем выше эффективность с точки зрения экономии ресурсов. Однако при этом снижается устойчивость к ошибкам. В помехоустойчивых кодах скорость часто составляет 1/2, 2/3, 3/4 и т.п. — это означает, что на каждый информационный бит приходится один, полтора или треть дополнительного (избыточного) бита.

Разнообразие кодов в повседневной практике

Чтобы увидеть масштаб применения теории, достаточно оглянуться вокруг.

QR-коды и штрихкоды — это наглядные примеры двумерного и одномерного представительского кодирования. Они преобразуют короткие текстовые или числовые сообщения в геометрические узоры, легко распознаваемые камерами и считывающими устройствами. QR-код содержит режимы данных (цифровой, буквенно-цифровой, байтовый, кандзи), указатели длины, данные, помехоустойчивые коды Рида–Соломона (в нескольких уровнях коррекции), а также маскировку и фиксированные паттерны для быстрой локализации и ориентации.

Номера — будь то ИНН, VIN автомобиля или банковский идентификатор — тоже являются кодами. Они структурированы по заранее оговорённым правилам, часто включают контрольные цифры (например, по алгоритму Луна), обеспечивающие защиту от случайных опечаток при ручном вводе.

Текст в компьютере — последовательность чисел, интерпретируемых согласно выбранной кодировке (Windows-1251, UTF-8 и т.д.). Переход от одного стандарта к другому — это смена кодовой книги.

Машинный код — это двоичное представление инструкций процессора. Каждая команда — это кодовое слово фиксированной или переменной длины, однозначно интерпретируемое исполнительным устройством.

Байт, как уже упоминалось, — базовая единица структурирующего кодирования в современных архитектурах.

Шифрованный код — особый класс, где цель кодирования — сокрытие смысла от неавторизованных лиц. Здесь кодовая книга (точнее, ключ) должен быть известен только отправителю и получателю. Хотя криптография тесно связана с теорией кодирования, формально она вынесена в отдельную дисциплину, так как её главная задача — обеспечение конфиденциальности, целостности и аутентичности.

Что подразумевают программисты под «написанием кода»?

В профессиональной среде фраза «я пишу код» почти всегда означает «я создаю исходный текст программы на языке программирования». Это — частный случай кодирования: преобразование алгоритмов и бизнес-логики в последовательность инструкций, понятных компилятору или интерпретатору.

Тем не менее, само понятие «исходный код» — это уже результат нескольких уровней кодирования:

  • На уровне мышления — идея → алгоритм (логическое кодирование).
  • На уровне проектирования — алгоритм → структура данных и модулей (архитектурное кодирование).
  • На уровне реализации — структура → текст на языке высокого уровня (языковое кодирование).
  • На уровне компиляции — текст → промежуточный код (например, IL в .NET или байт-код в Java) → машинный код (целевое кодирование).

Таким образом, даже «простое» написание программы — это многоступенчатый процесс кодирования, где каждый этап решает свою задачу: выразительность, проверяемость, переносимость, эффективность. Программист, осознанно или нет, работает с кодами — и понимание их природы помогает писать устойчивый, эффективный и сопровождаемый код.


Источниковое кодирование: сжатие как управление избыточностью

Любая информация, с которой сталкивается человек или машина, содержит в себе избыточность — структуру, повторяемость, предсказуемость. Язык полон шаблонов: в русском языке сочетание «жд» почти всегда следует за «о» («ожидание», «объезд»), в английском частота буквы e значительно выше, чем z. В изображениях соседние пиксели часто имеют близкие цвета; в аудиосигнале значения меняются плавно; в логах сервера одни и те же сообщения повторяются сотни раз в секунду.

Избыточность не является недостатком — напротив, она повышает устойчивость к ошибкам, облегчает восприятие и интерпретацию. Однако с точки зрения хранения и передачи она неэффективна: зачем тратить ресурсы на то, что можно восстановить по контексту?

Именно устранение такой «технической» избыточности — цель источникового кодирования. Этот термин подчёркивает: кодирование происходит на стороне источника данных, ещё до их отправки в канал связи. Ключевое требование — возможность точного (при сжатии без потерь) или приближённого (при сжатии с потерями) восстановления исходного содержания по сжатому представлению.

Энтропия как мера неопределённости и предел сжатия

Чтобы понять, насколько можно сжать данные, необходимо ввести количественную меру их «неожиданности». Такой мерой в теории информации служит энтропия, введённая Клодом Шенноном.

Интуитивно: энтропия высока, когда исходы равновероятны и непредсказуемы (например, результат броска честной монеты); энтропия низка, когда одни исходы встречаются гораздо чаще других (например, распределение букв в тексте). Чем ниже энтропия источника, тем больше в нём внутренней структуры — и тем сильнее потенциал сжатия.

Важнейший результат, известный как первая теорема Шеннона (или теорема об источниковом кодировании), утверждает следующее: для любого источника с конечной энтропией существует способ кодирования, при котором средняя длина кодового слова может быть сделана сколь угодно близкой к значению энтропии, но не может быть меньше него. Иными словами, энтропия — это фундаментальный нижний предел среднего числа бит на символ при сжатии без потерь.

Это не означает, что каждый конкретный файл можно сжать до энтропии в точности — теорема говорит об асимптотическом поведении для длинных последовательностей. Но она задаёт ориентир: если у вас есть текст с энтропией 4 бита на символ, то никакой алгоритм не сможет закодировать его в среднем короче 4 бит/символ, независимо от изощрённости метода. Практические алгоритмы стремятся приблизиться к этому пределу.

Принципы сжатия без потерь

Существует два основных подхода к построению эффективных источниковых кодов:

  1. Статистическое кодирование — строится на основе частотного распределения символов (или их комбинаций). Часто встречающиеся элементы кодируются короткими словами, редкие — длинными. Важно, чтобы ни одно кодовое слово не было префиксом другого — иначе декодирование станет неоднозначным. Такие коды называются префиксными.

  2. Словарное кодирование — строится на выявлении повторяющихся подстрок (фраз, блоков). Вместо повторной записи фрагмента используется ссылка на его предыдущее вхождение. Эффективность растёт с увеличением длины и регулярности повторов.

Эти подходы не исключают друг друга; на практике они комбинируются (например, в формате DEFLATE, лежащем в основе ZIP и PNG).

Код Хаффмана: оптимальное префиксное кодирование

Алгоритм Хаффмана — один из самых известных и изящных методов статистического кодирования. Он строит бинарное дерево, в листьях которого размещаются символы источника; глубина листа определяет длину его кодового слова. Чем выше частота символа — тем ближе он к корню и тем короче код.

Процесс построения:

  • Все символы рассматриваются как отдельные узлы с весом, равным их частоте.
  • На каждом шаге выбираются два узла с наименьшими весами, объединяются в новый узел, вес которого равен сумме весов потомков.
  • Процесс повторяется, пока не останется один корневой узел.
  • Код для каждого символа получается проходом от корня к листу, где левое ребро интерпретируется как 0, правое — как 1 (или наоборот).

Ключевое свойство: код Хаффмана оптимален среди всех префиксных кодов, использующих целое число бит на символ. Однако он не всегда достигает энтропийного предела, поскольку не может присвоить символу, скажем, 2.3 бита — только целое количество. Для преодоления этого ограничения применяются более тонкие методы.

Арифметическое кодирование: кодирование блоков как одного числа

Арифметическое кодирование работает иначе: вместо присвоения отдельного кодового слова каждому символу оно кодирует всю последовательность целиком как одно вещественное число в интервале от 0 до 1.

Идея в следующем: исходный интервал [0, 1) разбивается на подынтервалы, пропорциональные вероятностям символов. При чтении первого символа выбирается соответствующий подынтервал, который затем снова разбивается — уже в соответствии с вероятностями второго символа, и так далее. После обработки всей последовательности остаётся достаточно узкий интервал, и любое число внутри него однозначно идентифицирует исходную строку.

Преимущество: средняя длина кода может быть сколь угодно близка к энтропии, даже если она нецелочисленная (например, 4.17 бит/символ). Недостатки — большая вычислительная сложность и чувствительность к ошибкам: искажение одного бита в потоке может сделать невозможным декодирование всей оставшейся части.

В современных системах арифметическое кодирование часто заменяется его приближением — кодированием по методу диапазонов (range coding), которое оперирует целыми числами фиксированной разрядности и допускает эффективную реализацию на целочисленной арифметике.

Словарные методы: LZ77, LZ78, LZW

Семейство алгоритмов Лемпеля–Зива (по фамилиям авторов Abraham Lempel и Jacob Ziv) основано на идее динамического построения словаря из уже встреченных фрагментов данных.

LZ77 (1977) использует скользящее окно: текущая позиция делится на «просмотренное» (словарь) и «ещё не обработанное» (буфер упреждения). Алгоритм ищет в словаре самую длинную строку, совпадающую с началом буфера, и заменяет её тройкой: (смещение, длина, следующий символ). Смещение — на сколько позиций назад находится начало совпадения; длина — насколько далеко совпадение продолжается; следующий символ — первый символ после совпадения, обеспечивающий продвижение.

Метод эффективен при наличии локальных повторов (например, в программном коде, XML, логах). Реализации LZ77 — основа форматов gzip, zlib, PNG.

LZ78 (1978) строит явный словарь: каждое новое слово формируется как ранее встречавшаяся фраза плюс один символ. На выходе — последовательность ссылок на номер фразы в словаре и нового символа. Словарь растёт по мере обработки.

LZW (Lempel–Ziv–Welch, 1984) — улучшенная версия LZ78, предложенная Терри Уэлчем. Отличается тем, что изначально словарь содержит все односимвольные строки, и новые фразы добавляются до их фактического использования в выходном потоке. Это устраняет необходимость передавать отдельный «новый символ» в каждом элементе выхода — достаточно номера фразы. За счёт этого LZW достигает высокой скорости и умеренной степени сжатия.

LZW долгое время был стандартом в форматах GIF и TIFF (в режиме сжатия), хотя из-за патентных ограничений в 1990-х его применение вызывало юридические сложности. Сегодня патенты истекли, и метод остаётся актуальным как учебный и встраиваемый вариант.

Сжатие с потерями: управляемая деградация

Когда точное восстановление не требуется — например, для изображений, звука, видео — применяется сжатие с потерями. Здесь кодирование включает этап квантования: преобразование данных в область, где можно отбросить наименее значимые компоненты без критичного ухудшения восприятия.

Классическая схема (например, в JPEG):

  1. Преобразование — данные переводятся в другое представление (часто — частотное, например, через дискретное косинусное преобразование). В такой области энергия концентрируется в немногих коэффициентах, остальные близки к нулю.

  2. Квантование — коэффициенты делятся на заданные значения («таблицу квантования») и округляются до целых. Чем грубее квантование — тем больше потерь, но и тем сильнее сжатие.

  3. Постобработка — полученные целые числа подвергаются источниковому кодированию без потерь (например, комбинации RLE и кода Хаффмана).

Важно: потери вносятся до этапа сжатия без потерь. Последний лишь устраняет оставшуюся избыточность в уже квантованных данных.

Сжатие с потерями не имеет строгого энтропийного предела — вместо него используются субъективные и объективные метрики качества: PSNR, SSIM для изображений; MOS, PESQ для аудио. Выбор точки баланса «размер — качество» зависит от задачи: видеонаблюдение допускает сильное сжатие, медицинская визуализация — нет.


Помехоустойчивое кодирование: избыточность как защита

В идеальном мире каждый бит, отправленный в канал, приходит к получателю без изменений. В реальности же физические носители несовершенны: шум в линии связи, магнитные флуктуации на жёстком диске, космическое излучение в памяти спутника, царапины на поверхности DVD — всё это способно инвертировать отдельные биты или даже целые блоки данных.

Если данные передаются «как есть», то даже однократная ошибка может быть катастрофической: изменение одного бита в исполняемом файле делает его неработоспособным; ошибка в банковском переводе приводит к неверной сумме; искажение координат в телеметрии — к потере аппарата.

Чтобы противостоять этому, в данные намеренно вводится избыточность — дополнительные биты, не несущие прямой информационной нагрузки, но содержащие сведения о структуре и взаимосвязях исходных данных. Эта избыточность позволяет не просто обнаружить факт искажения, но и — при достаточном объёме — исправить его, восстановив первоначальное сообщение без повторной передачи.

Ключевой принцип: расстояние между кодовыми словами. Чем дальше (в смысле количества различающихся позиций) находятся допустимые слова друг от друга, тем больше ошибок можно пережить, не перепутав одно слово с другим.

Расстояние Хэмминга и минимальное расстояние кода

Расстояние Хэмминга между двумя кодовыми словами одинаковой длины — это число позиций, в которых они различаются. Например, между 0110 и 0011 расстояние равно 2 (второй и четвёртый биты).

Для всего кода можно определить минимальное расстояние Хэмминга — наименьшее расстояние между любыми двумя различными кодовыми словами в нём. Эта величина — главный параметр, определяющий корректирующую способность кода.

Пусть d — минимальное расстояние кода. Тогда:

  • Код способен обнаружить до d – 1 ошибок: любая комбинация из d – 1 инверсий не переведёт одно корректное слово в другое корректное.
  • Код способен исправить до t ошибок, где t = ⌊(d – 1)/2⌋: даже если в кодовом слове изменилось до t бит, оно останется ближе (по Хэммингу) к исходному слову, чем к любому другому допустимому.

Таким образом, чтобы исправлять одну ошибку, требуется d ≥ 3; чтобы исправлять две — d ≥ 5, и так далее. Повышение минимального расстояния достигается за счёт увеличения длины кодового слова при фиксированном числе информационных битов — то есть за счёт снижения скорости кода.

Контрольная сумма и циклические избыточные коды

Простейший способ обнаружения ошибок — контрольная сумма: функция от данных, добавляемая к сообщению. При получении вычисляется та же функция и сравнивается с присланной. Несовпадение указывает на искажение.

Однако обычная арифметическая сумма (например, XOR всех байтов) слабо защищает от типичных ошибок: две инвертированных единицы в одном разряде компенсируют друг друга, и ошибка остаётся незамеченной.

Гораздо эффективнее циклические избыточные коды (CRC). Они основаны на интерпретации сообщения как коэффициентов многочлена над полем GF(2) (где сложение — XOR, умножение — AND) и делении этого многочлена на заранее оговорённый порождающий многочлен фиксированной степени.

Остаток от деления (обычно длиной 16, 32 или 64 бита) и приписывается к сообщению. При приёме процедура повторяется: если остаток нулевой — с высокой вероятностью ошибок нет.

CRC не исправляет ошибки, но обнаруживает:

  • все однобитные ошибки;
  • все двубитные ошибки (при правильно подобранном порождающем многочлене);
  • все нечётные ошибки (если многочлен содержит множитель x + 1);
  • все пакетные ошибки длиной, не превышающей степень многочлена.

CRC — стандарт де-факто для проверки целостности на канальном и транспортном уровнях: Ethernet (CRC-32), ZIP-архивы, протоколы Modbus, SMB и многие другие.

Код Хэмминга: первый практически реализуемый корректирующий код

Ричард Хэмминг в 1950 году предложил код, способный исправлять одиночные ошибки и обнаруживать двойные при минимальной избыточности. Его конструкция элегантна и иллюстрирует ключевые идеи теории.

Основная идея — распределение контрольных битов по позициям, номера которых являются степенями двойки (1, 2, 4, 8, …), а информационные биты — по остальным. Каждый контрольный бит отвечает за чётность (XOR) подмножества позиций, в двоичном представлении которых установлен соответствующий бит номера.

Например, бит в позиции 1 (2⁰) проверяет все позиции, где младший бит номера равен 1: 1, 3, 5, 7, …
Бит в позиции 2 (2¹) проверяет позиции с установленным вторым битом: 2, 3, 6, 7, 10, 11, …
Бит в позиции 4 (2²) — позиции 4–7, 12–15 и т.д.

При приёме вычисляются все контрольные суммы. Получаемый синдром (набор значений проверок) интерпретируется как двоичное число — и указывает номер позиции, в которой произошла ошибка (если синдром ≠ 0). Если ошибка одиночная — её легко исправить инверсией бита в этой позиции. Если ошибок две — синдром ненулевой, но не соответствует ни одной позиции; в этом случае возможна только фиксация факта двойной ошибки (без исправления).

Код Хэмминга имеет скорость k / (k + r), где r — число контрольных битов, k — информационных, и 2ʳ ≥ k + r + 1. Для 4 информационных битов достаточно 3 контрольных (7,4)-код; для 11 бит — 4 контрольных (15,11)-код и т.д.

Несмотря на возраст, код Хэмминга применяется до сих пор — например, в ECC-памяти (Error-Correcting Code RAM), особенно в однобитовом варианте SEC (Single Error Correction), иногда расширенном до SEC-DED (Single Error Correction, Double Error Detection) за счёт добавления глобального бита чётности.

Линейные и циклические коды: алгебраическая структура как основа

Код Хэмминга — частный случай линейных кодов. Линейный код — это подпространство векторного пространства над конечным полем (обычно GF(2)). Это означает, что:

  • нулевой вектор (все нули) всегда принадлежит коду;
  • сумма (по модулю 2) любых двух кодовых слов также является кодовым словом.

Благодаря линейности код можно задавать порождающей матрицей G (размером k × n), через которую любое информационное слово u длины k преобразуется в кодовое слово c = u·G. Альтернативно — проверочной матрицей H (размером (n–k) × n), для которой любое корректное слово удовлетворяет условию c·Hᵀ = 0.

Если, кроме того, циклический сдвиг любого кодового слова также принадлежит коду, то код называется циклическим. Такие коды особенно удобны для аппаратной реализации, поскольку операции сдвига и обратной связи естественны для регистров сдвига.

CRC, упомянутый ранее, основан именно на циклических кодах. Порождающий многочлен определяет структуру кода, а вычисление остатка — эквивалентно умножению на и делению по модулю порождающего многочлена.

Коды Рида–Соломона: исправление пакетных ошибок

Коды Рида–Соломона (РС), предложенные в 1960 году, — один из самых влиятельных классов недвоичных линейных кодов. В отличие от двоичных кодов (где символ — бит), в РС-кодах символ — элемент конечного поля GF(2ᵐ), то есть m-битовое слово (часто m = 8, тогда символ = байт).

Ключевая особенность: РС-код длины n = 2ᵐ – 1 с k информационными символами способен исправить до t = (n – k)/2 ошибочных символов — независимо от того, сколько бит в каждом из них повреждено. Это делает РС-коды исключительно эффективными против пакетных ошибок — когда биты искажаются группами (например, царапина на диске повреждает подряд идущие байты).

Принцип работы основан на интерполяции многочленов: информационные символы интерпретируются как значения многочлена степени k–1 в первых k точках поля; оставшиеся n–k значений — это избыточные символы, вычисленные как значения того же многочлена в других точках. Искажённые символы соответствуют «выбросам» в таблице значений; алгоритмы (Берлекэмпа–Мэсси, Форни) позволяют восстановить исходный многочлен, зная, что он имеет ограниченную степень.

Применения РС-кодов повсеместны:

  • CD, DVD, Blu-ray: комбинация РС-кодов (CIRC — Cross-Interleaved Reed–Solomon Code) обеспечивает устойчивость к царапинам длиной до нескольких миллиметров.
  • QR-коды: четыре уровня коррекции (L, M, Q, H) — это РС-коды с разной степенью избыточности (от 7% до 30% от общего объёма).
  • DVB, ADSL, WiMAX: защита транспортных потоков.
  • RAID-6: поддержка отказа двух дисков одновременно за счёт двух независимых РС-проверок.
  • Космическая связь (Voyager, Cassini, Mars rovers): РС-коды в связке с свёрточными.

Современные коды: LDPC и Turbo-коды — приближение к пределу Шеннона

До 1990-х считалось, что практические коды существенно отстают от теоретического предела — пропускной способности канала, заданной второй теоремой Шеннона. Этот предел утверждает: для любого канала с шумом существует код, при котором вероятность ошибки может быть сделана сколь угодно малой, если скорость кода не превышает пропускной способности канала.

Прорыв произошёл с появлением итеративных кодов:

  • Turbo-коды (1993) — построены на параллельном (или последовательном) каскадировании двух (или более) свёрточных кодеров, разделённых перемежителем (перестановкой битов). Декодирование ведётся итеративно: два декодера обмениваются «мягкой» информацией (вероятностными оценками), уточняя гипотезы на каждом проходе. Turbo-коды впервые позволили приблизиться к пределу Шеннона в пределах 0.5 дБ — что считалось невозможным.

  • LDPC-коды (Low-Density Parity-Check, низкоплотностные коды с проверкой на чётность) — предложены ещё Галлагером в 1960-х, но «воскресли» в 1990-х благодаря вычислительной мощности. Их проверочная матрица H содержит очень мало единиц (низкая плотность), что позволяет строить эффективные итеративные алгоритмы декодирования на основе передачи сообщений (belief propagation). LDPC масштабируемы, параллелизуемы и достижимы на практике скорости, отличающиеся от шенноновского предела менее чем на 0.1 дБ.

Оба семейства легли в основу современных стандартов:

  • Turbo-коды: 3G (UMTS), 4G (LTE) — в каналах управления и данных.
  • LDPC-коды: 5G NR (для данных), Wi-Fi 6/6E/7 (802.11ax/be), DVB-S2X, 10GBase-T Ethernet.

Разница в подходах: Turbo-коды проектируются «сверху вниз» — сначала архитектура, потом оптимизация; LDPC — «снизу вверх»: случайная или псевдослучайная генерация разреженной матрицы с последующей проверкой спектральных свойств. Оба требуют значительных вычислительных ресурсов при декодировании, но обеспечивают беспрецедентную надёжность в условиях низкого отношения сигнал/шум.

Где применяются корректирующие коды на практике?

  • Память: ECC RAM (SEC/SEC-DED), NAND Flash (BCH, LDPC — из-за высокой плотности и износа ячеек).
  • Хранилища: RAID 5/6 (чётность и РС), ZFS, Btrfs (встроенные проверки целостности + коррекция), SSD-контроллеры (многоуровневые схемы: CRC + LDPC).
  • Связь: мобильные сети (Turbo, LDPC), спутниковая (РС + свёрточные), оптоволокно (FEC — Forward Error Correction на физическом уровне).
  • Беспроводные интерфейсы: Bluetooth (Hamming, CRC), LoRa (специфические свёрточные коды).
  • Цифровое вещание: DVB-T2 (LDPC + BCH), ATSC 3.0.

Важный принцип проектирования: каскадное кодирование. Редко используется один код «от начала до конца». Гораздо чаще применяется стек: например, внутренний код высокой скорости для исправления редких ошибок (LDPC), внешний — более мощный, но медленный, для «дочистки» (РС). Между ними — перемежение, чтобы преобразовать пакетные ошибки в независимые, удобные для внутреннего декодера.


Практическое воплощение кодирования

Как устроен штрихкод: линейная упорядоченность как основа

Штрихкод — один из самых ранних и массовых примеров машинно-читаемого представительского кодирования. Его суть — преобразование числового идентификатора (например, GTIN — Global Trade Item Number) в последовательность чёрных штрихов и белых пробелов переменной ширины.

Наиболее распространённый стандарт — EAN-13 (13 цифр):

  • Первая цифра — префикс страны (условный — присваивается GS1, не всегда совпадает с географией).
  • Следующие пять — код производителя.
  • Ещё пять — код товара.
  • Последняя — контрольная цифра, вычисленная по алгоритму, обеспечивающему обнаружение одиночных ошибок и большинства транспозиций.

Само изображение строится по строгой схеме:

  • Каждая цифра кодируется семью модулями (элементарными единицами ширины), комбинацией двух штрихов и двух пробелов (например, 0001101 → три узких белых, два узких чёрных, один узкий белый, один узкий чёрный).
  • Левые шесть цифр кодируются по одной из двух таблиц («чётная» и «нечётная»), что позволяет сканеру определять ориентацию (слева направо или справа налево).
  • Между левой и правой половинами — фиксированный центральный разделитель (01010).
  • По краям — боковые разделители (101), обозначающие начало и конец.

Штрихкод не содержит данных о товаре — только идентификатор. Вся семантика (название, цена, вес) хранится в базе данных и извлекается по ID после сканирования. Это — пример разделения представления и содержания: код служит ключом, а не контейнером.

Для расширения возможностей появились двумерные коды.

QR-код: двумерная геометрия, многоуровневая защита и гибкость формата

QR-код (Quick Response Code) — решётка модулей (обычно чёрных квадратов на белом фоне), способная кодировать числа, буквы, байты и даже иероглифы. Его ключевые особенности — быстрое распознавание (за счёт фиксированных паттернов) и встроенная коррекция ошибок.

Структура QR-кода (версия 1–40, где версия v даёт размер (4v + 17) × (4v + 17) модулей):

  1. Фиксированные элементы:

    • Три квадрата-маркера в углах (размер 7×7 модулей), каждый окружён рамкой. Позволяют быстро обнаружить и локализовать код под любым углом и масштабом.
    • Хронирующая линия («зебра») по горизонтали и вертикали между маркерами — задаёт модульную сетку.
    • Малый маркер выравнивания (в версиях ≥2) — компенсирует искажения при печати и сканировании.
  2. Маскировка: чтобы избежать длинных однородных областей (которые мешают детекторам), данные накладываются маской — одной из восьми предопределённых битовых шаблонов (например, шахматка, полосы). Наилучшая маска выбирается по эвристике (минимум «плохих» структур), её номер сохраняется в служебной области.

  3. Режимы данных: перед самими данными указывается индикатор режима:

    • Цифровой (0–9, по три цифры на 10 бит),
    • Буквенно-цифровой (0–9, A–Z, пробел, $%*+-./:, по два символа на 11 бит),
    • Байтовый (любые 8-битные значения, один символ на 8 бит),
    • Кандзи (для японских иероглифов, компактное кодирование Shift_JIS).
  4. Данные и избыточность:

    • Сначала идёт счётчик длины (разной разрядности в зависимости от режима и версии),
    • Затем — данные, преобразованные согласно режиму,
    • Далее — терминатор (четыре нуля, если место остаётся),
    • Заполнение — чередующиеся байты 11101100 и 00010001, чтобы добить объём до нужного размера.
  5. Коррекция ошибок — коды Рида–Соломона:

    • Данные и заполнение разбиваются на блоки,
    • К каждому блоку добавляются проверочные символы (количество зависит от уровня коррекции: L — 7%, M — 15%, Q — 25%, H — 30%),
    • Блоки перемежаются (interleaving), чтобы локальная порча (царапина) затрагивала несколько блоков понемногу — что повышает шансы на восстановление.

Такая многослойность делает QR-код устойчивым даже при 30%-ном повреждении — и при этом легко распознаваемым камерами бюджетных смартфонов.

Кодирование на уровне физического хранения

Под капотом носителей — ещё один слой кодирования, невидимый пользователю, но критичный для надёжности.

Жёсткие диски (HDD)

Запись на магнитный диск — это чередование смен полярности магнитного домена. Однако прямая запись 0101… привела бы к частым переключениям и шуму. Поэтому применяется линейное кодирование — преобразование битового потока в последовательность, удобную для магнитной записи:

  • RLL (Run-Length Limited) — например, RLL(2,7): между двумя единицами должно быть от 2 до 7 нулей. Это ограничивает минимальную и максимальную длину «тихих» участков, улучшая синхронизацию и плотность записи.

Твердотельные накопители (SSD)

В NAND-ячейках количество циклов записи ограничено, и уровень заряда со временем дрейфует. Здесь используются:

  • Коды BCH (для старых и бюджетных SSD) — циклические коды, исправляющие до 8–16 бит на 512 байт.
  • LDPC (в современных SSD) — адаптивные декодеры, учитывающие износ ячеек и изменяющие стратегию по мере старения блока.

Кроме того, SSD применяют выравнивание износа (wear leveling) и сборку мусора (garbage collection) — это уже логическое кодирование: перераспределение логических адресов по физическим ячейкам, чтобы избежать преждевременного отказа.

Оптические диски (CD/DVD)

Как уже упоминалось, используется CIRC — каскад из двух кодов Рида–Соломона с перемежением. При этом данные ещё и модулируются: 8-битные байты преобразуются в 14-битные слова (EFM — Eight-to-Fourteen Modulation), в которых гарантировано от 2 до 10 нулей между единицами. Это упрощает тактовую синхронизацию при чтении лазером.

Протоколы и форматы: кодирование как договорённость

Любой протокол — это кодовая книга, зафиксированная в спецификации.

  • HTTP/2 и HTTP/3 используют HPACK и QPACK — алгоритмы сжатия заголовков на основе динамического и статического словарей (разновидность LZ + индексация), чтобы избежать повторной передачи User-Agent, Content-Type и т.д.

  • Protocol Buffers, Apache Avro, MessagePack — системы сериализации: преобразование структурированных данных (объектов, записей) в компактный бинарный поток. Здесь сочетаются:

    • тегирование полей (каждое поле — номер + значение),
    • переменная длина целых (varint — младшие 7 бит + флаг продолжения),
    • дельта-кодирование для массивов,
    • схема как внешняя кодовая книга.
  • Base64 — представительское кодирование: преобразование бинарных данных в текстовый алфавит (A–Z, a–z, 0–9, +, /), безопасный для передачи через текстовые каналы (email, JSON, XML). На каждые 3 байта исходных данных приходится 4 символа Base64 — избыточность 33%, но зато гарантируется отсутствие управляющих символов.

Кодирование в повседневной работе программиста

Для разработчика «кодирование» — это написание инструкций и постоянное принятие решений о форме представления данных:

  • Выбор кодировки текста (UTF-8 vs UTF-16 vs Windows-1251) — это выбор кодовой книги для символов. UTF-8 доминирует благодаря обратной совместимости с ASCII и эффективности для латиницы; UTF-16 — в системах, ориентированных на Unicode с самого начала (Java, Windows API); однобайтные кодировки — в унаследованных системах.

  • Сериализация/десериализация — преобразование объектов в последовательность байтов и обратно. Здесь важно: порядок байтов (endianness), выравнивание, обработка ссылок и циклов. Ошибки на этом уровне приводят к «битому» состоянию, нестабильности, уязвимостям (deserialization attacks).

  • Хеширование и контрольные суммыдетерминированное кодирование фиксированной длины. CRC32 для быстрой проверки целостности, xxHash для хеширования в хеш-таблицах, MurmurHash — в распределённых системах. Каждый алгоритм — компромисс между скоростью, равномерностью распределения и устойчивостью к коллизиям.

  • Обфускация — намеренное затруднение чтения кода без изменения его поведения. Имена переменных заменяются на a, b1, структура — на спагетти-код. Это тоже кодирование: исходный код → функционально эквивалентный, но нечитаемый код. Хотя формально не защищает от реверс-инжиниринга, повышает порог входа.

  • Компиляция и транспляция — многоступенчатое кодирование:
    Исходный код → AST (абстрактное синтаксическое дерево) → промежуточное представление (IL, байт-код) → машинный код (или WebAssembly) → микрооперации процессора.
    На каждом этапе — оптимизации, переупорядочивание, развёртывание циклов, инлайнинг. Программа остаётся той же по смыслу, но её представление меняется радикально.

Кодирование и человек: интерфейсы, документация, обучение

В «Вселенной IT» важно помнить: кодирование — не только машинная операция. Оно присутствует везде, где есть передача смысла.

  • Документация — кодирование знаний на естественном языке с использованием структур (заголовки, списки, диаграммы), соглашений (GOST, Google Developer Documentation Style Guide) и метафор.

  • UI/UX-дизайн — визуальное кодирование: иконки как символы, цвета как сигналы (красный — ошибка, зелёный — успех), расположение как иерархия.

  • Обучение — адаптивное кодирование: перевод сложных концепций (например, расстояния Хэмминга) в истории, аналогии, интерактивные модели — чтобы соответствовать когнитивным возможностям аудитории.